Skip to content

feat: embed Coder agent chat in VS Code sidebar panel#844

Open
ThomasK33 wants to merge 13 commits intomainfrom
embed-chat-panel
Open

feat: embed Coder agent chat in VS Code sidebar panel#844
ThomasK33 wants to merge 13 commits intomainfrom
embed-chat-panel

Conversation

@ThomasK33
Copy link
Member

Summary

Adds a new Coder Chat webview panel that embeds the Coder agent chat UI directly inside VS Code, triggered via a deep link.

Deep link format

vscode://coder.coder-remote/openChat?url=<coderUrl>&token=<sessionToken>&agentId=<chatAgentId>

How it works

  1. The /openChat URI handler calls setupDeployment() to authenticate, then opens the chat panel.
  2. ChatPanelProvider renders a webview containing an iframe pointing at /agents/{id}/embed on the Coder server.
  3. The embed page detects the user is signed out and sends postMessage({ type: "coder:vscode-ready" }) to the parent.
  4. The webview relays this to the extension host, which replies with the session token.
  5. The webview forwards { type: "coder:vscode-auth-bootstrap", payload: { token } } into the iframe.
  6. The embed page calls API.setSessionToken(token), fetches user + permissions, and renders the authenticated chat UI.

Why not cookies?

Cookie-based auth (SameSite=None; Secure) does not work in VS Code webview iframes over HTTP. The setSessionToken approach uses header-based auth via axios, bypassing cookies and CSRF entirely.

Files changed

  • package.json — Added coderChat view container (panel, not activity bar) and coder.chatPanel webview view.
  • src/webviews/chat/chatPanelProvider.ts (new)ChatPanelProvider class: postMessage relay, openChat(agentId), iframe HTML generation.
  • src/uri/uriHandler.ts — Added /openChat route and handleOpenChat() handler.
  • src/extension.ts — Creates and registers ChatPanelProvider, passes it to the URI handler.

Testing

Verified end-to-end against dev.coder.com: deep link → extension activation → postMessage auth → full chat UI rendered with multi-turn conversation and tool invocations.


This PR was authored with Coder Agents 🤖

@ThomasK33 ThomasK33 marked this pull request as draft March 17, 2026 13:31
@ThomasK33 ThomasK33 marked this pull request as draft March 17, 2026 13:31
Add a Coder Chat sidebar that opens via deep link:
  vscode://coder.coder-remote/openChat?url=...&token=...&agentId=...

The chat panel embeds /agents/:agentId/embed in an iframe through a
local reverse proxy (needed to work around VS Code webview sandbox
restrictions). Auth is handled via postMessage — the iframe signals
readiness, the extension sends the session token, and the iframe sets
it as an axios header for all API requests.

- chatPanelProvider.ts: WebviewViewProvider with EmbedProxy and
  postMessage relay for auth bootstrap
- uriHandler.ts: /openChat route that reads agentId and opens the panel
- extension.ts: registers the provider and wires it into the URI handler
- package.json: coderChat view container (panel area, no activity bar
  icon) with coder.chatPanel webview view
The local reverse proxy was added to work around VS Code's webview
sandbox blocking script execution in nested cross-origin iframes.
Testing confirms the sandbox restriction does not apply when the
iframe loads directly from the Coder server URL, so the proxy is
unnecessary complexity.

Auth continues to flow via postMessage+setSessionToken (no cookies,
no CSRF, no proxy header injection).
- Remove unused getErrorHtml() (dead after proxy removal).
- Extract renderView() to deduplicate resolveWebviewView/refresh.
- Fix package.json indentation (extra nesting introduced in prior commit).
The existing /open deep link now optionally accepts an agentId query
parameter. When present, the chat panel opens alongside the workspace.
Old extensions silently ignore unknown query params, so this is fully
backwards compatible — no error dialog, no version negotiation needed.

The standalone /openChat route is kept for cases where no workspace
context is needed (e.g. direct links from the agents page).
@ThomasK33 ThomasK33 marked this pull request as ready for review March 17, 2026 15:41
@ThomasK33 ThomasK33 requested a review from EhabY March 17, 2026 15:42
Comment on lines +142 to +148
const iframe = document.getElementById('chat-frame');
const status = document.getElementById('status');

iframe.addEventListener('load', () => {
iframe.style.display = 'block';
status.style.display = 'none';
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be made more flexible so that even if messages arrive mid-run it wouldn't cause the status to always be displayed and never cleared (we could have some state tracking in this webview possibly). Anyway if this is unlikely or too much work then we can skip

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a great idea for a follow-up, but it's not needed for this now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ties with the auth comment above, so maybe they should be tackled at the same time when needed!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, though I'd argue that it's less a matter of when it's needed and more a matter of if it's needed.

const routes: Readonly<Record<string, UriRouteHandler>> = {
"/open": handleOpen,
"/openDevContainer": handleOpenDevContainer,
"/openChat": handleOpenChat,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we trigger this one actually?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Via a direct deep link

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant through the Coder UI, I could do /open but not /openChat, is it not implemented yet in coder/coder?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The updated deep links are not yet part of coder/coder. I want to get the extension changes merged first, in case the deep link needs to change.
I'm happy to remove the /openChat and just leave /open for now.

Comment on lines +70 to +79
if (!this.agentId) {
webview.html = this.getNoAgentHtml();
return;
}

const coderUrl = this.client.getHost();
if (!coderUrl) {
webview.html = this.getNoAgentHtml();
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I keep getting the no agent HTML even though I have a chat running, am I doing something wrong 🤔 ?

commands.open() triggers vscode.openFolder with a remote authority,
which reloads the window and wipes in-memory state.  To survive this,
handleOpen now saves the agentId to globalState before the reload.
After the reload, activate() reads and clears it once the deployment
is configured, then opens the chat panel.

This follows the same set-and-clear pattern used by firstConnect.
No reason for it to be in a separate ctx.subscriptions.push block —
chatPanelProvider is already created above.
Copy link
Member Author

Addressed in 5c933a8 — replaced the this.view! non-null assertion with an explicit throw new Error("renderView called before resolveWebviewView").

Worth noting: there are zero code paths in this PR that can actually trigger this throw — both callers (resolveWebviewView and refresh) guarantee this.view is set before calling renderView(). So this is technically dead code, but it's a reasonable defensive guard.

The /open route with an optional agentId param is the only deep link
path for opening the chat panel. The standalone /openChat route and
the chatPanelProvider dependency in the URI handler are no longer
needed — the chat panel is opened from extension.ts after the
remote-authority reload picks up the persisted agentId.
@ThomasK33 ThomasK33 requested a review from EhabY March 18, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants